﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RaytracerLib.MathLib.Polynomials;
using RaytracerLib.MathLib;
using RaytracerLib.Rays;
using YAXLib;
using RaytracerLib.UVMappers;
using RaytracerLib.MathLib.PolynomialFunction;
using NLog;

namespace RaytracerLib.RenderableObjects
{
    public class PolynomialSurface : RenderableObject
    {
        protected PolynomialFunction m_polynomial;
        private Func<Vector3, Vector3> m_gradient; 
        private Func<Vector3, Vector3, Polynomial> m_intersection;
        private PolynomialSolver m_solver;

        [YAXNode]
        public string Equation;

        [YAXNode]
        public string Parameter1 = "";

        [YAXNode]
        public string Parameter2 = "";

        [YAXNode]
        public string Parameter3 = "";

        [YAXNode]
        public AABB LocalBoundBox;

        private Vector3 m_aabb_minimum;
        private Vector3 m_aabb_maximum;

        public PolynomialSurface(Vector3 a_right, Vector3 a_up) :
            base(a_right, a_up)
        {
            Name = "Polynomial surface";
            m_aabb_minimum = Vector3.MAXIMUM;
            m_aabb_maximum = Vector3.MINIMUM;
            Closed = false;
            LocalBoundBox = AABB.INFINITY;
            m_uv_mapper = new SphericalUVMapper();

            Update(UpdateFlags.All);
        }

        internal override Intersection GetIntersection(Intersection a_source_ray_intersection, Ray a_ray)
        {
            Vector3 local_dir;
            Vector3 local_start;
            bool back_hit = false;

            if (!TransformToLocalToBoundBox(a_ray, out local_dir, out local_start))
                return Scene.NoIntersection;

            if ((a_source_ray_intersection != null) && (a_source_ray_intersection.SceneObject == this))
            {
                if (a_ray.RaySurfaceSide == RaySurfaceSide.RefractedSide)
                {
                    local_start += ((WorldToLocalNormal * a_source_ray_intersection.Normal).Normalized) *
                        -Constants.POLYNOMIAL_SURFACE_FROM_MOVE_DELTA;
                }
                else
                {
                    local_start += ((WorldToLocalNormal * a_source_ray_intersection.Normal).Normalized) *
                        Constants.POLYNOMIAL_SURFACE_FROM_MOVE_DELTA;
                }
            }

            Polynomial p = m_intersection(local_start, local_dir);

            double dist = m_solver.Solve(p);

            if (dist == Double.PositiveInfinity)
                return Scene.NoIntersection;

            Vector3 local_pos = local_start + local_dir * dist;

            Vector3 normal = m_gradient(local_pos);

            back_hit = normal * local_dir > 0;

            if (back_hit && OneSide)
                return Scene.NoIntersection;

            Vector3 world_pos = LocalToWorld * local_pos;
            double world_dist = (world_pos - a_ray.Start).Length;

            Intersection intersection = new Intersection()
            {
                PrevIntersection = a_source_ray_intersection, 
                SceneObject = this,
                SourceRay = a_ray,
                LocalPos = local_pos,
                Dist = world_dist,
                Scene = Scene,
                BackHit = back_hit,
                Pos = world_pos
            };

            // Numerical errors.
            if (intersection.Normal * intersection.SourceRay.Dir >= 0)
                return Scene.NoIntersection;

                m_aabb_minimum = Vector3.Minimize(local_pos, m_aabb_minimum);
                m_aabb_maximum = Vector3.Maximize(local_pos, m_aabb_maximum);

            return intersection;
        }

        internal override Vector3 GetNormal(Intersection a_intersection)
        {
            if (a_intersection.BackHit)
                return (LocalToWorldNormal * -m_gradient(a_intersection.LocalPos)).Normalized;
            else
                return (LocalToWorldNormal * m_gradient(a_intersection.LocalPos)).Normalized;
        }

        internal override Vector3 GetUVW(Intersection a_intersection)
        {
            return base.GetUVW(a_intersection) * 0.5 + Vector3.HALF;
        }

        public override string ToString()
        {
            return String.Format("PolynomialSurface: {0}", Name);
        }

        internal override void GetTangents(Intersection a_intersection, out Vector3 a_tangent_x,
            out Vector3 a_tangent_y)
        {
            if (a_intersection.BackHit)
            {
                a_tangent_x = Vector3.CrossProduct(
                    Up, a_intersection.Normal).Normalized;
                a_tangent_y = Vector3.CrossProduct(
                    a_tangent_x, a_intersection.Normal).Normalized;
            }
            else
            {
                a_tangent_x = Vector3.CrossProduct(
                    a_intersection.Normal, Up).Normalized;
                a_tangent_y = Vector3.CrossProduct(
                    a_intersection.Normal, a_tangent_x).Normalized;
            }
        }

        protected override void RenderEnd(bool a_all)
        {
            base.RenderEnd(a_all);

            if (!a_all)
                return;

            if (LocalBoundBox != AABB.INFINITY)
            {
                if ((!LocalBoundBox.Minimum.IsAlmostRelativeEquals(m_aabb_minimum)) &&
                    (!LocalBoundBox.Maximum.IsAlmostRelativeEquals(m_aabb_maximum)))
                {
                    LogManager.GetLogger("Raytracer").Error("Bound box for {0}", Name);
                    LogManager.GetLogger("Raytracer").Error("Actually: {0}", LocalBoundBox);
                    LogManager.GetLogger("Raytracer").Error("Should be: {0}", new AABB(m_aabb_minimum, m_aabb_maximum));
                }
            }
        }

        protected override AABB GetLocalBoundBox()
        {
            return LocalBoundBox;
        }

        protected override void RenderStart(RenderStartPhase a_phase)
        {
            base.RenderStart(a_phase);

            if (a_phase == RenderStartPhase.PrepareObjectToRender)
            {
                string equation = ApplyParameters(Equation);
                m_polynomial = new PolynomialFunction(equation);
                m_gradient = m_polynomial.Gradient;
                m_intersection = m_polynomial.Intersection;

                m_solver = new PolynomialSolver(PolynomialSolveMethod.Sturm,
                    Scene.RenderOptions.RootFindingMethod);

                m_aabb_minimum = Vector3.MAXIMUM;
                m_aabb_maximum = Vector3.MINIMUM;
            }
        }

        private string ApplyParameters(string a_equation)
        {
            a_equation = ApplyParameter(a_equation, Parameter1);
            a_equation = ApplyParameter(a_equation, Parameter2);
            a_equation = ApplyParameter(a_equation, Parameter3);

            return a_equation;
        }

        private string ApplyParameter(string a_equation, string a_parameter)
        {
            if (String.IsNullOrWhiteSpace(a_parameter))
                return a_equation;

            string[] ar = a_parameter.Split('=');

            if (ar.Length < 2)
                return a_equation;

            a_equation = a_equation.Replace(ar[0], "(" + ar[1].Replace(",", ".") + ")");

            return a_equation;

        }

        public override void ScaleAbsolute(double a_scale)
        {
            Scale *= a_scale;

            base.ScaleAbsolute(a_scale);
        }
    }
}
